package encryption;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import model.Candidate;
import model.VoteResult;

public class Paillier {
	private static boolean flag = true;
	private static BigInteger mue;
	private static BigInteger nBig;
	private static BigInteger n2Big;
	private static BigInteger p;
	private static BigInteger q;
	private static BigInteger gBig;
	private static BigInteger lambdaBig;

	private static BigInteger message;

	private static BigInteger Nv;
	public static BigInteger base;
	private static BigInteger Mmax;
	private static BigInteger Tmax;
	private static int candidatesNo;

	private static ArrayList<BigInteger> votesList;

	// private static String zoma;
	// private static String anwar;
	// private static String bola;

	private static List<Candidate> candidates = new ArrayList<Candidate>();

	// private static List<String> candiNames = new ArrayList<String>();
	// private static List<BigInteger> candiVals = new ArrayList<BigInteger>();

	public static List<Candidate> getCandidates() {
		return candidates;
	}

	public static void main(String[] args) {

		// ArrayList<String> arr = new ArrayList<String>();

		// arr.add("bola");
		// arr.add("anwar");
		// arr.add("zoma");

		// generateKeys(BigInteger.valueOf(73), arr);

		// printInfo();

		// zoma = "zoma";
		// anwar = "anwar";
		// bola = "bola";

		// System.out.println("Start the voting ...");

		// for(int i =0;i<20;i++)
		// vote(bola);
		// for(int i =0;i<20;i++)
		// vote(zoma);
		// for(int i =0;i<20;i++)
		// vote(anwar);

		// getVotersPerCandidate();
	}

	public static List<VoteResult> getVotersPerCandidate(List<String> votes) {

		votesList = new ArrayList<BigInteger>();

		int j = 0;
		for (String vote : votes) {
			if (vote != null)
				votesList.add(BigInteger.valueOf(Long.parseLong(vote)));
			System.out.println(votesList.get(j));
			j++;
		}
		BigInteger encryptedResults = calculateVotes();
		System.out.println("Results before decryption: " + encryptedResults);
		// message =
		// BigInteger.valueOf(Integer.parseInt(extractResult(encryptedResults)));
		message = extractResult(encryptedResults);

		System.out.println("Result message: " + message);

		List<VoteResult> results = new ArrayList<VoteResult>();
		BigInteger temp1;
		int i = candidates.size() - 1;

		while (i >= 0) {
			temp1 = message.divide(base.pow(i));
			message = message.subtract(temp1.multiply(base.pow(i)));

			VoteResult newVoteResult = new VoteResult();
			newVoteResult.setCandidateName(candidates.get(i).getName());
			newVoteResult.setNumberOfVotes(temp1.intValue());
			results.add(newVoteResult);
			System.out.println(candidates.get(i).getName() + " : " + temp1);
			i--;
		}
		return results;
	}

	public static BigInteger vote(BigInteger candidateBigIntCode,
			BigInteger[] publicKey) {

		BigInteger candiTemp = BigInteger.ZERO;
		System.out.println("size:" + candidates.size());
		//
		// for (String candi : candiNames) {
		// if (candi.equalsIgnoreCase(candidate)) {
		// System.out.println(candi+"\t"+candidate+"E");
		// candiTemp = candiVals.get(candiNames.indexOf(candi));
		// }
		// }

		// candiTemp = BigInteger.valueOf(candidateIndex);

		System.out.println("Encrypting vote for candidate: "
				+ candidateBigIntCode);
		BigInteger encryptedVote = encrypt(candidateBigIntCode, publicKey[0],
				publicKey[1]);
		System.out.println("Encrypted message: " + encryptedVote);

		// votesList.add(encryptedVote);
		return encryptedVote;
	}

	public static BigInteger calculateVotes() {
		System.out.println("Calculating votes ...");

		BigInteger votes = BigInteger.ONE;
		Iterator<BigInteger> itr = votesList.iterator();

		while (itr.hasNext())
			votes = votes.multiply(itr.next());

		votes = votes.mod(n2Big);
		return votes;
	}

	public static BigInteger extractResult(BigInteger encryptedResults) {
		System.out.println("Decrypting message ...");
		return decrypt(encryptedResults).mod(nBig);
	}

	/**
	 * 
	 * generates new keys. note, the get private and public key methods have to
	 * be called to get the new key pairs.
	 * 
	 * @param voters
	 *            - the number of voters
	 * @param candidates
	 *            - An <code>ArrayList</code> of <code>String</code>s containing
	 *            the candidate names
	 * 
	 * @see Paillier#getPrivateKey()
	 * @see Paillier#getPublicKey()
	 */
	public static void generateKeys(BigInteger voters,
			List<Candidate> candidatesList) {
		//
		// for (Candidate candidate : candidates) {
		// candiNames.add(candidate.getName());
		// }

		// candiNames = candidates;

		candidates = candidatesList;
		candidatesNo = candidates.size();

		Nv = voters;
		base = Nv.add(BigInteger.ONE);
		Mmax = base.pow(candidatesNo - 1);
		Tmax = Nv.multiply(Mmax);

		for (int i = 0; i < Paillier.candidates.size(); i++) {
			candidates.get(i).setBigIntCode(base.pow(i));
		}

		p = BigInteger.ZERO;
		q = BigInteger.ZERO;
		// votesList = new ArrayList<BigInteger>();

		generatePQ();
		System.out.println("got p & q");

		calculateVariables();
	}

	public static BigInteger decrypt(BigInteger c) {
		return c.modPow(lambdaBig, n2Big).subtract(BigInteger.ONE).divide(nBig)
				.multiply(mue).mod(nBig);
	}

	/**
	 * key used for encryption.
	 * 
	 * key[0] = n, key[1] = mue
	 * 
	 * @return the private key.
	 */
	public static BigInteger[] getPrivateKey() {
		BigInteger[] key = new BigInteger[2];
		key[0] = lambdaBig;
		key[1] = mue;
		return key;
	}

	/**
	 * key used for decryption.
	 * 
	 * key[0] = lambda, key[1] = g
	 * 
	 * @return the public key.
	 */
	public static BigInteger[] getPublicKey() {
		BigInteger[] key = new BigInteger[2];
		key[0] = nBig;
		key[1] = gBig;
		return key;
	}

	private static void calculateVariables() {

		BigInteger alpha = BigInteger.ONE;
		BigInteger beta = BigInteger.ONE;

		n2Big = nBig.multiply(nBig);

		System.out.println("n2 = " + n2Big);

		lambdaBig = ((p.subtract(BigInteger.ONE)).multiply(q
				.subtract(BigInteger.ONE))).divide(gcd(p
				.subtract(BigInteger.ONE), q.subtract(BigInteger.ONE)));

		System.out.println("lambda = " + lambdaBig);

		/*
		 * Random r = new Random();
		 * 
		 * do { do { alpha = new BigInteger("" + r.nextInt(Integer.MAX_VALUE));
		 * }while(alpha.compareTo(BigInteger.ZERO) < 0 || alpha.compareTo(nBig)
		 * >= 0 || !alpha.gcd(nBig).equals(BigInteger.ONE)); do { beta = new
		 * BigInteger("" + r.nextInt(Integer.MAX_VALUE));
		 * }while(beta.compareTo(BigInteger.ZERO) < 0 || beta.compareTo(nBig) >=
		 * 0 || !beta.gcd(nBig).equals(BigInteger.ONE));
		 * 
		 * }while(beta.compareTo(BigInteger.ZERO) < 0 || beta.compareTo(alpha)
		 * >= 0 || !beta.gcd(alpha).equals(BigInteger.ONE));
		 */
		System.out.println("alpha = " + alpha);
		System.out.println("beta = " + beta);

		BigInteger alphan = (alpha.multiply(nBig)).add(BigInteger.ONE);

		System.out.println("alphan = " + alphan);

		BigInteger betapowern = beta.pow(nBig.intValue());

		System.out.println("betapowern = " + betapowern.intValue());

		gBig = (alphan.multiply(betapowern)).mod(n2Big);

		System.out.println("g = " + gBig);

		if (gBig.compareTo(BigInteger.ZERO) < 0) {
			gBig = gBig.multiply(BigInteger.valueOf(-1));
		}

		mue = gBig.modPow(lambdaBig, n2Big).subtract(BigInteger.ONE).divide(
				nBig).modInverse(nBig);
	}

	public static BigInteger encrypt(BigInteger message, BigInteger n,
			BigInteger g) {

		System.out.println("message " + message);
		Random rnd = new Random();
		BigInteger c = BigInteger.ONE;
		BigInteger r = BigInteger.ONE;
		int bitLength = 7;
		BigInteger n2 = n.multiply(n);

		do {

			// do {
			// r = new BigInteger("" + rnd.nextInt(Integer.MAX_VALUE));
			// } while (r.compareTo(BigInteger.ZERO) < 0 || r.compareTo(n) >= 0
			// || !r.gcd(n).equals(BigInteger.ONE));
			do {
				r = BigInteger.probablePrime(bitLength, rnd);
			} while (r.compareTo(n) > 0);

			BigInteger gToM = g.pow(message.intValue());
			BigInteger rToN = r.pow(n.intValue());

			BigInteger gByR = gToM.multiply(rToN);

			c = gByR.mod(n.pow(2));
			//System.out.println("Here");

		} while (c.compareTo(BigInteger.ZERO) < 0 || c.compareTo(n2) >= 0
				|| !c.gcd(n2).equals(BigInteger.ONE));

		return c;
	}

	private static void generatePQ() {

		int bitLength = (int) Math.ceil(Math.log(Math.sqrt(Tmax.intValue()))
				/ Math.log(2));
		System.out.println("Bitlength " + bitLength);

		Random rnd = new Random();
		while (flag) {
			p = BigInteger.probablePrime(bitLength, rnd);
			q = BigInteger.probablePrime(bitLength, rnd);
			checkKey(p, q);
		}
	}

	public static void printInfo() {
		System.out.println("p = " + p);
		System.out.println("q = " + q);
		System.out.println("lambda = " + lambdaBig);
		System.out.println("g = " + gBig);
		System.out.println("mue = " + mue);
	}

	private static void checkKey(BigInteger a1, BigInteger a2) {
		System.out.println("P = " + a1 + " Q = " + a2);

		BigInteger x = a1.multiply(a2);
		BigInteger y = (a1.subtract(BigInteger.ONE)).multiply(a2
				.subtract(BigInteger.ONE));

		nBig = a1.multiply(a2);

		if ((x.gcd(y).equals(BigInteger.ONE)) && !a1.equals(a2)
				&& nBig.compareTo(Tmax) > 0) {
			flag = false;
			nBig = p.multiply(q);
		}
	}

	private static BigInteger gcd(BigInteger a, BigInteger b) {
		if (b.equals(BigInteger.ZERO))
			return a;
		else
			return gcd(b, a.mod(b));
	}
}
